package com.atguigu.gmall.product.redis;

import com.atguigu.gmall.common.constant.RedisConst;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 自定义注解切面类,对使用@GmallCache注解方法进行增强
 *
 * @author: atguigu
 * @create: 2023-06-13 10:56
 */
@Slf4j
@Aspect //标识为切面类(将增强对指定一些方法(切入点)进行增强)
@Component
public class GmallCacheAspect {


    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 环绕增强,对使用注解@GmallCache方法所在类产生代理对象,增强方法逻辑功能
     * 切入点表达式 @execution(* com.atguigu.*.*(..))
     * @param pjp 切入点
     * @return
     */
    @SneakyThrows
    @Around("@annotation(com.atguigu.gmall.product.redis.GmallCache)")
    public Object cacheAndLockAspect(ProceedingJoinPoint pjp) {
        try {
            //0.定义返回结果-封装各种业务数据
            Object objectResult = new Object();
            //一.前置通知
            log.info("前置通知");
            //1.优先从分布式缓存Redis获取业务数据
            //1.1 组装业务数据key 问题:怎么获取注解中属性?  答案:反射
            //1.1.1 获取切入点方法 方法签名
            MethodSignature signature = (MethodSignature) pjp.getSignature();
            //1.1.2 获取方法信息
            Method method = signature.getMethod();
            //1.1.3 获取方法上注解
            GmallCache annotation = method.getAnnotation(GmallCache.class);
            //1.1.4 获取注解内部属性
            String prefix = annotation.prefix();
            String suffix = annotation.suffix();
            //1.1.5 获取方法参数 业务key中间标识
            String methodArgs = "none";
            //1.1.6 获取方法参数 将参数值获取到
            List<Object> objects = Arrays.asList(pjp.getArgs());
            if (!CollectionUtils.isEmpty(objects)) {
                methodArgs = objects.stream().map(arg -> arg.toString()).collect(Collectors.joining(":"));
            }
            String dataKey = prefix + methodArgs + suffix;

            //1.2 从Redis中获取缓存业务数据
            objectResult = redisTemplate.opsForValue().get(dataKey);

            //2.命中缓存则直接响应业务数据
            if (objectResult != null) {
                return objectResult;
            } else {
                //3.未命中缓存则调用尝试获取分布式锁
                //3.1 拼接锁key 减少锁粒度,为不同查询业务参数设置各自锁
                String lockKey = prefix + methodArgs + RedisConst.SKULOCK_SUFFIX;
                //3.2 创建锁对象
                RLock lock = redissonClient.getLock(lockKey);
                //3.3 获取分布式锁
                boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX2, RedisConst.SKULOCK_EXPIRE_PX1, TimeUnit.SECONDS);
                if (flag) {
                    try {
                        //二.执行目标方法(查询数据库方法)
                        objectResult = pjp.proceed();
                        //三.后置通知
                        log.info("后置通知");
                        //4.将查询数据库结果放入缓存
                        if (objectResult != null) {
                            //4.1 查询数据库有结果,将结果放入缓存 业务数据基础时间24小时+随机值(缓存雪崩)
                            int randNum = new Random().nextInt(600);
                            Long time = RedisConst.SKUKEY_TIMEOUT + randNum;
                            redisTemplate.opsForValue().set(dataKey, objectResult, time, TimeUnit.SECONDS);
                            return objectResult;
                        } else {
                            //4.2 查询数据库没有结果,将结果放入缓存
                            redisTemplate.opsForValue().set(dataKey, objectResult, 5, TimeUnit.MINUTES);
                            return objectResult;
                        }
                    } finally {
                        //5.释放锁
                        lock.unlock();
                    }
                } else {
                    //获取锁 自旋(保证用户请求线程一定能获取到业务数据)
                    TimeUnit.MILLISECONDS.sleep(500);
                    this.cacheAndLockAspect(pjp);
                }
            }
            return objectResult;
        } catch (Throwable e) {
            //6.当锁服务不可以,兜底处理方案查询数据库
            return pjp.proceed();
        }
    }
}
